Skip to content

fix(images): backport the image[] and mask[] form key drop to rc/1.102.0 (#39512) - #41697

Merged
mateo-berri merged 2 commits into
rc/1.102.0from
litellm_backport_39512_rc_1_102_0
Sep 17, 2026
Merged

mateo-berri merged 2 commits into
rc/1.102.0from
litellm_backport_39512_rc_1_102_0

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Backport of #39512 onto rc/1.102.0, so Saturday's stable v1.102.0 carries the fix (cherry-pick of merge commit 289c52b, applied clean, no rc-only changes)

Problem this solves:

  • /v1/images/edits forwarded the raw image[] key to providers
  • OpenAI 400s the documented alias: "Invalid type for 'image[0]'"
  • mask[] 400s the same way: "This parameter already has a different value"
  • v1.102.0-rc.2, the current rc tip, still carries the regression

How it solves it:

User Flow

Before: a developer following LiteLLM's own documented image[] form gets a 400 and no edited image

  1. They send POST https://litellm-domain/v1/images/edits with model=gpt-image-1, image[]=@tree.png, and prompt=add a small red hat on top, exactly the form the endpoint's own docs show
  2. They get back HTTP 400: Invalid type for 'image[0]': expected a file, but got a string instead.
  3. They drop the brackets and send the same request with image=@tree.png, which returns HTTP 200 with an edited image
  4. They add a mask the documented way, sending image=@tree.png plus mask[]=@mask.png, and get back HTTP 400: Invalid parameter: 'mask'. This parameter already has a different value in your parameters.
  5. They drop those brackets too and send mask=@mask.png, which returns HTTP 200, so the documented bracketed form is unusable through the gateway on either field

After: the same documented image[] and mask[] requests come back with an edited image

  1. They send POST https://litellm-domain/v1/images/edits with model=gpt-image-1, image[]=@tree.png, and prompt=add a small red hat on top, exactly the form the endpoint's own docs show
  2. They get back HTTP 200 with a b64_json image and a usage block
  3. They send the same request with image=@tree.png, which returns HTTP 200 with an edited image
  4. They add a mask the documented way, sending image=@tree.png plus mask[]=@mask.png, and get back HTTP 200 with an edited image
  5. They send mask=@mask.png instead and get HTTP 200 as well, so both spellings work on both fields
  6. Sending image and image[] together still returns HTTP 422 Cannot specify both 'image' and 'image[]', so the existing guard against ambiguous requests is unchanged

Relevant issues

Backport of #39512, which fixes #41421

Affected release

regression in v1.100.0 (PR #38104), still present in v1.102.0-rc.2. #39512 carries the backport-stable label and this PR is that cherry-pick onto rc/1.102.0, so the fix lands before the stable is tagged

Linear ticket

Resolves LIT-6816

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.); the three red CircleCI jobs are rc-line test rot, see Caveats
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Both legs run on the rc line: Before is the rc/1.102.0 tip 3f965660a1 (what v1.102.0-rc.2 ships), After is this PR's tip 0e03b62ab8. Same proxy config against the real OpenAI API with gpt-image-2.5-sunburst, two uvicorn workers (--num_workers 2) on each proxy, and the same 256x256 tree.png / tree2.png / tree3.png / mask.png set as #39512's run on main. The customer's own call shape is the OpenAI Python SDK's client.images.edit(image=[...]), which the SDK sends as repeated image[] parts, so the SDK cases come first and the curl cases cover the documented bracketed forms directly. Real spend on every 200. Same nine cases in the same order on both sides

config.yaml:

model_list:
  - model_name: gpt-image-2.5-sunburst
    litellm_params:
      model: openai/gpt-image-2.5-sunburst
      api_key: os.environ/OPENAI_API_KEY

general_settings:
  master_key: sk-lit6816

sdk_edit.py:

import sys, json
from openai import OpenAI, APIStatusError
base_url, model, mode = sys.argv[1], sys.argv[2], sys.argv[3]
client = OpenAI(base_url=base_url, api_key="sk-lit6816")
files = {"single": open("tree.png", "rb"), "list1": [open("tree.png", "rb")], "list3": [open("tree.png", "rb"), open("tree2.png", "rb"), open("tree3.png", "rb")]}[mode]
try:
    r = client.images.edit(model=model, image=files, prompt="add a small red hat on top of the tree", size="1024x1024", quality="low")
    print(json.dumps({"created": r.created, "usage_total_tokens": r.usage.total_tokens if r.usage else None, "b64_json_length": len(r.data[0].b64_json or "")}))
except APIStatusError as e:
    print(f"HTTP {e.status_code}", json.dumps(e.body)[:400])

Each case is the command followed by its output. A 200 is summarized to created, usage.total_tokens, the b64_json length, and the number of images returned; an error shows the proxy's response body

Before (3f96566)

SDK: three images in a list (the customer flow)

$ python sdk_edit.py http://127.0.0.1:PORT gpt-image-2.5-sunburst list3   # OpenAI SDK: client.images.edit(model=..., image=<list3>, prompt=..., size="1024x1024", quality="low")
HTTP 400 {"message": "litellm.BadRequestError: OpenAIException - {\n  \"error\": {\n    \"message\": \"Invalid type for 'image[0]': expected a file, but got a string instead.\",\n    \"type\": \"invalid_request_error\",\n    \"param\": \"image[0]\",\n    \"code\": \"invalid_type\"\n  }\n}. Received Model Group=gpt-image-2.5-sunburst\nAvailable Model Group Fallbacks=None", "type": "invalid_request_error", "

SDK: a list of one image

$ python sdk_edit.py http://127.0.0.1:PORT gpt-image-2.5-sunburst list1   # OpenAI SDK: client.images.edit(model=..., image=<list1>, prompt=..., size="1024x1024", quality="low")
HTTP 400 {"message": "litellm.BadRequestError: OpenAIException - {\n  \"error\": {\n    \"message\": \"Invalid type for 'image[0]': expected a file, but got a string instead.\",\n    \"type\": \"invalid_request_error\",\n    \"param\": \"image[0]\",\n    \"code\": \"invalid_type\"\n  }\n}. Received Model Group=gpt-image-2.5-sunburst\nAvailable Model Group Fallbacks=None", "type": "invalid_request_error", "

SDK: a single image, not a list

$ python sdk_edit.py http://127.0.0.1:PORT gpt-image-2.5-sunburst single   # OpenAI SDK: client.images.edit(model=..., image=<single>, prompt=..., size="1024x1024", quality="low")
{"created": 1789687525, "usage_total_tokens": 478, "b64_json_length": 1145840}

curl: two image[] parts

$ curl -s http://127.0.0.1:PORT/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image[]=@tree.png" -F "image[]=@tree2.png" -F "size=1024x1024" -F "quality=low"
HTTP 400 {"error": {"message": "litellm.BadRequestError: OpenAIException - {\n  \"error\": {\n    \"message\": \"Invalid type for 'image[0]': expected a file, but got a string instead.\",\n    \"type\": \"invalid_request_error\",\n    \"param\": \"image[0]\",\n    \"code\": \"invalid_type\"\n  }\n}. Received Model Group=gpt-image-2.5-sunburst\nAvailable Model Group Fallbacks=None", "type": "invalid_request

curl: one image[] part

$ curl -s http://127.0.0.1:PORT/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image[]=@tree.png" -F "size=1024x1024" -F "quality=low"
HTTP 400 {"error": {"message": "litellm.BadRequestError: OpenAIException - {\n  \"error\": {\n    \"message\": \"Invalid type for 'image[0]': expected a file, but got a string instead.\",\n    \"type\": \"invalid_request_error\",\n    \"param\": \"image[0]\",\n    \"code\": \"invalid_type\"\n  }\n}. Received Model Group=gpt-image-2.5-sunburst\nAvailable Model Group Fallbacks=None", "type": "invalid_request

curl: canonical image part

$ curl -s http://127.0.0.1:PORT/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image=@tree.png" -F "size=1024x1024" -F "quality=low"
HTTP 200 {"created": 1789687534, "usage_total_tokens": 478, "b64_json_length": 1140288, "images": 1}

curl: image plus mask[]

$ curl -s http://127.0.0.1:PORT/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image=@tree.png" -F "mask[]=@mask.png" -F "size=1024x1024" -F "quality=low"
HTTP 400 {"error": {"message": "litellm.BadRequestError: OpenAIException - {\n  \"error\": {\n    \"message\": \"Invalid parameter: 'mask'. This parameter already has a different value in your parameters. Please check your parameters.\",\n    \"type\": \"invalid_request_error\",\n    \"param\": \"mask\",\n    \"code\": \"invalid_parameter\"\n  }\n}. Received Model Group=gpt-image-2.5-sunburst\nAvailable Mo

curl: image plus canonical mask

$ curl -s http://127.0.0.1:PORT/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image=@tree.png" -F "mask=@mask.png" -F "size=1024x1024" -F "quality=low"
HTTP 200 {"created": 1789687545, "usage_total_tokens": 478, "b64_json_length": 1113548, "images": 1}

curl: image and image[] in one request (rejected on both sides)

$ curl -s http://127.0.0.1:PORT/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image=@tree.png" -F "image[]=@tree2.png" -F "size=1024x1024" -F "quality=low"
HTTP 422 {"detail": "Cannot specify both 'image' and 'image[]'"}

After (0e03b62)

The tip has since moved to 24d4453, which only adds a pytest.MonkeyPatch annotation on the three backported tests, so it cannot change proxy behavior and the run above still stands

SDK: three images in a list (the customer flow)

$ python sdk_edit.py http://127.0.0.1:PORT gpt-image-2.5-sunburst list3   # OpenAI SDK: client.images.edit(model=..., image=<list3>, prompt=..., size="1024x1024", quality="low")
{"created": 1789687397, "usage_total_tokens": 1010, "b64_json_length": 1142456}

SDK: a list of one image

$ python sdk_edit.py http://127.0.0.1:PORT gpt-image-2.5-sunburst list1   # OpenAI SDK: client.images.edit(model=..., image=<list1>, prompt=..., size="1024x1024", quality="low")
{"created": 1789687411, "usage_total_tokens": 478, "b64_json_length": 1145692}

SDK: a single image, not a list

$ python sdk_edit.py http://127.0.0.1:PORT gpt-image-2.5-sunburst single   # OpenAI SDK: client.images.edit(model=..., image=<single>, prompt=..., size="1024x1024", quality="low")
{"created": 1789687422, "usage_total_tokens": 478, "b64_json_length": 1125640}

curl: two image[] parts

$ curl -s http://127.0.0.1:PORT/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image[]=@tree.png" -F "image[]=@tree2.png" -F "size=1024x1024" -F "quality=low"
HTTP 200 {"created": 1789687432, "usage_total_tokens": 744, "b64_json_length": 1137656, "images": 1}

curl: one image[] part

$ curl -s http://127.0.0.1:PORT/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image[]=@tree.png" -F "size=1024x1024" -F "quality=low"
HTTP 200 {"created": 1789687441, "usage_total_tokens": 478, "b64_json_length": 1141424, "images": 1}

curl: canonical image part

$ curl -s http://127.0.0.1:PORT/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image=@tree.png" -F "size=1024x1024" -F "quality=low"
HTTP 200 {"created": 1789687452, "usage_total_tokens": 478, "b64_json_length": 1140744, "images": 1}

curl: image plus mask[]

$ curl -s http://127.0.0.1:PORT/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image=@tree.png" -F "mask[]=@mask.png" -F "size=1024x1024" -F "quality=low"
HTTP 200 {"created": 1789687462, "usage_total_tokens": 478, "b64_json_length": 1124220, "images": 1}

curl: image plus canonical mask

$ curl -s http://127.0.0.1:PORT/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image=@tree.png" -F "mask=@mask.png" -F "size=1024x1024" -F "quality=low"
HTTP 200 {"created": 1789687473, "usage_total_tokens": 478, "b64_json_length": 1122276, "images": 1}

curl: image and image[] in one request (rejected on both sides)

$ curl -s http://127.0.0.1:PORT/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image=@tree.png" -F "image[]=@tree2.png" -F "size=1024x1024" -F "quality=low"
HTTP 422 {"detail": "Cannot specify both 'image' and 'image[]'"}

Type

🐛 Bug Fix

Caveats (if any)

Low

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

@greptile-apps

greptile-apps Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no actionable issue remains in the reviewed changes.

Summary

This backport prevents normalized image[] and mask[] multipart aliases from also being forwarded to image providers under their bracketed keys.

  • Introduces shared constants for bracketed image-edit file fields.
  • Filters bracketed aliases from provider-bound request data while retaining canonical image and mask content.
  • Adds regression coverage for bracketed aliases and canonical file fields.
  • The changes since the previous review strongly type all three new monkeypatch parameters, fully addressing the resolved prior finding.

Reviews (2) · Last reviewed commit: "test(images): type the monkeypatch fixtu..."

Comment thread tests/test_litellm/proxy/image_endpoints/test_endpoints.py Outdated
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 17, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 24d4453. Configure here.

@mateo-berri
mateo-berri merged commit e1ef8b3 into rc/1.102.0 Sep 17, 2026
52 of 56 checks passed
@mateo-berri
mateo-berri deleted the litellm_backport_39512_rc_1_102_0 branch September 17, 2026 23:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant